﻿/*
 * 什么是向量？具有大小（magnitude）和方向的量，就是向量。
 * 当向量以某个点为起点，旋转 360 度的时候，将构成一个圆，那个点就是圆心，向量尾部的坐标就是圆的边。
 * 因此，想要让物体A 围绕着物体 B 旋转的时候，只要旋转向量，然后加上物体 B 的坐标即可。
 * 所以：摄像机的位置 = 目标位置 + 向量（偏移量）。
 */

using UnityEngine;
/// <summary>
/// 3D 视角摄像机跟随。
/// </summary>
/// <summary>
/// 3D 视角摄像机跟随。
/// </summary>
public class SmoothFollowTargetCamera : MonoBehaviour
{
    /// <summary>
    /// 目标位置。
    /// </summary>
    public Transform _target;

    /// <summary>
    /// 摄像机与目标的相对位置的向量长度。
    /// </summary>
    public float _relativeCameraPositionDistance = 7.0f;

    /// <summary>
    /// 摄像机的新位置。
    /// </summary>
    private Vector3 _newCameraPosition;

    /// <summary>
    /// 主相机与目标物体之间的最大距离。
    /// </summary>
    public float _distanceMax = 9.0f;

    /// <summary>
    /// 主相机与目标物体之间的最小距离。
    /// </summary>
    public float _distanceMin = 1.0f;

    /// <summary>
    /// 平滑速度。
    /// </summary>
    public float _smoothSpeed = 2f;

    /// <summary>
    /// 鼠标滚轮灵敏度（备注：鼠标滚轮滚动后将调整相机与目标物体之间的间隔）。
    /// </summary>
    public float _mouseScrollWheelSensitivity = 5.0f;

    /// <summary>
    /// 摄像机的 X 轴的角度值。
    /// </summary>
    private float _eulerAnglesX = 0.0f;

    /// <summary>
    /// 摄像机的 Y 轴的角度值。
    /// </summary>
    private float _eulerAnglesY = 15.0f;

    /// <summary>
    /// 主相机水平方向旋转速度。
    /// </summary>
    public float _xSpeed = 1000.0f;

    /// <summary>
    /// 主相机纵向旋转速度。
    /// </summary>
    public float _ySpeed = 1000.0f;

    /// <summary>
    /// 垂直滚动最大的角度值。
    /// </summary>
    public int _yMaxLimit = 80;

    /// <summary>
    ///  垂直滚动最小的角度值。
    /// </summary>
    public int _yMinLimit = 0;

    private void Start()
    {
    }

    private void Update()
    {
        //通过滚动鼠标滚轮来获得距离的增（减）量。
        var changeDistance = Input.GetAxis("Mouse ScrollWheel");

        //如果不等于 0，则表示已经滚动了鼠标滚轮。
        if (changeDistance != 0)
        {
            //新的相对距离 = 原距离 + 偏移量 X 倍率。并限制最小距离与最大距离。
            _relativeCameraPositionDistance = Mathf.Clamp(_relativeCameraPositionDistance - changeDistance * _mouseScrollWheelSensitivity, _distanceMin, _distanceMax);
        }

        //将向量按指定的角度旋转。
        Vector3 direction = GetQuaternion();

        //摄像机的位置 = 目标位置 - 向量（偏移量） X 向量长度。
        _newCameraPosition = GetTargetPosition() - direction * _relativeCameraPositionDistance;

        //检测摄像机新位置与玩家之间是否存在障碍物，如果存在，则重新计算摄像机的位置。
        CheckObstacleAndResetCameraPosition();

        //更新摄像机位置。
        transform.position = _newCameraPosition;

        //摄像机始终面向目标。
        transform.LookAt(GetTargetPosition());
    }

    #region 计算旋转角度

    /// <summary>
    /// 计算旋转角度。
    /// </summary>
    /// <returns></returns>
    private Vector3 GetQuaternion()
    {
        //旋转角度。
        var inputX = 0f;
        var inputY = 0f;

        if (Input.GetMouseButton(1))
        {
            inputX = Input.GetAxis("Mouse X");
            inputY = -Input.GetAxis("Mouse Y");
        }

        return GetQuaternion(inputX, inputY);
    }

    /// <summary>
    /// 计算旋转角度。
    /// </summary>
    /// <param name="inputX"></param>
    /// <param name="inputY"></param>
    /// <returns></returns>
    private Vector3 GetQuaternion(float inputX, float inputY)
    {
        //通过一个值来调整旋转的量（示例：输入旋转 10 度，xSpeed = 100，则实际旋转了 100 X 0.02 X 10 = 20 度角）。
        _eulerAnglesX += inputX * _xSpeed * Time.deltaTime;
        _eulerAnglesY += inputY * _ySpeed * Time.deltaTime;

        //限制 Y 轴的角度值限制最小值和最大值。
        _eulerAnglesY = ClampAngle(_eulerAnglesY, _yMinLimit, _yMaxLimit);

        //计算出旋转的角度。
        Quaternion quaternion = Quaternion.Euler(_eulerAnglesY, _eulerAnglesX, 0);

        //将向量按指定的角度旋转。
        Vector3 direction = quaternion * Vector3.forward;

        return direction;
    }

    /// <summary>
    /// 把角度限制到给定范围内。
    /// </summary>
    /// <param name="angle">角度。</param>
    /// <param name="min">最小角度。</param>
    /// <param name="max">最大角度。</param>
    /// <returns>角度。</returns>
    private float ClampAngle(float angle, float min, float max)
    {
        while (angle < -360)
        {
            angle += 360;
        }

        while (angle > 360)
        {
            angle -= 360;
        }

        return Mathf.Clamp(angle, min, max);
    }

    #endregion 计算旋转角度

    /// <summary>
    /// 获得目标物体的当前位置。
    /// </summary>
    /// <returns></returns>
    private Vector3 GetTargetPosition()
    {
        //获得目标物体的位置。
        var currentPosition = _target.position;

        /*
         * 目标的位置抬高（即：y 轴方向）一定的距离，这里使用的是物体的高度。
         * 原因：由于当前物体的原点是在物体的底部，所以摄像机会对着物体底部，但是现在希望的是对准物体的中央。
         *       所以经常细调后，还是对着物体的头顶比较合适。
         */
        currentPosition.y = currentPosition.y + _target.localScale.y * 1.5f;

        return currentPosition;
    }

    #region 碰到障碍物时调整摄像机位置

    private void CheckObstacleAndResetCameraPosition()
    {
        //摄像机的初始位置。
        var standardPos = _newCameraPosition;

        //寻找正确的位置（能看到角色的位置）并更新摄像机的位置。
        ViewingPositionCheck(standardPos);
    }

    /// <summary>
    /// 判断指定的位置是否可以看到角色，如果可以则更新摄像机的位置。
    /// </summary>
    /// <param name="checkPosition">指定的位置。</param>
    /// <returns>可以看到角色则返回 True，否则返回 False。</returns>
    private bool ViewingPositionCheck(Vector3 checkPosition)
    {
        //目标位置为起点。
        var startPosition = GetTargetPosition();

        //方向 = 摄像机位置 - 目标位置。
        var direction = checkPosition - startPosition;

        //目标位置与摄像机位置之间的距离。
        var cameraPositionDistance = direction.magnitude;

        //由目标位置为起点，向摄像机位置发出一条射线。
        if (Physics.Raycast(startPosition, direction, out var hit, cameraPositionDistance))
        {
            //当光线投射碰撞到某个对象时，要检测碰撞位置是否为摄像机位置。
            if (hit.transform != _target)
            {
                //当光线碰撞到的对象不是摄像机时，设置碰撞的位置为摄像机当前的位置。
                _newCameraPosition = hit.point;

                return true;
            }
        }

        //如果没有碰撞到任何物体或者碰撞到的是摄像机位置，则更新摄像机位置为当前检测的位置。
        _newCameraPosition = checkPosition;

        return true;
    }

    #endregion 碰到障碍物时调整摄像机位置
}